/*
 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.awt.event;

import sun.awt.AWTAccessor;
import sun.awt.AppContext;
import sun.awt.SunToolkit;

import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.font.TextHitInfo;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import java.lang.annotation.Native;

/**
 * Input method events contain information about text that is being
 * composed using an input method. Whenever the text changes, the
 * input method sends an event. If the text component that's currently
 * using the input method is an active client, the event is dispatched
 * to that component. Otherwise, it is dispatched to a separate
 * composition window.
 *
 * <p>
 * The text included with the input method event consists of two parts:
 * committed text and composed text. Either part may be empty. The two
 * parts together replace any uncommitted composed text sent in previous events,
 * or the currently selected committed text.
 * Committed text should be integrated into the text component's persistent
 * data, it will not be sent again. Composed text may be sent repeatedly,
 * with changes to reflect the user's editing operations. Committed text
 * always precedes composed text.
 *
 * @author JavaSoft Asia/Pacific
 * @since 1.2
 */
public class InputMethodEvent extends AWTEvent {

  /**
   * Serial Version ID.
   */
  private static final long serialVersionUID = 4727190874778922661L;

  /**
   * Marks the first integer id for the range of input method event ids.
   */
  @Native
  public static final int INPUT_METHOD_FIRST = 1100;

  /**
   * The event type indicating changed input method text. This event is
   * generated by input methods while processing input.
   */
  @Native
  public static final int INPUT_METHOD_TEXT_CHANGED = INPUT_METHOD_FIRST;

  /**
   * The event type indicating a changed insertion point in input method text.
   * This event is
   * generated by input methods while processing input if only the caret changed.
   */
  @Native
  public static final int CARET_POSITION_CHANGED = INPUT_METHOD_FIRST + 1;

  /**
   * Marks the last integer id for the range of input method event ids.
   */
  @Native
  public static final int INPUT_METHOD_LAST = INPUT_METHOD_FIRST + 1;

  /**
   * The time stamp that indicates when the event was created.
   *
   * @serial
   * @see #getWhen
   * @since 1.4
   */
  long when;

  // Text object
  private transient AttributedCharacterIterator text;
  private transient int committedCharacterCount;
  private transient TextHitInfo caret;
  private transient TextHitInfo visiblePosition;

  /**
   * Constructs an <code>InputMethodEvent</code> with the specified
   * source component, type, time, text, caret, and visiblePosition.
   * <p>
   * The offsets of caret and visiblePosition are relative to the current
   * composed text; that is, the composed text within <code>text</code>
   * if this is an <code>INPUT_METHOD_TEXT_CHANGED</code> event,
   * the composed text within the <code>text</code> of the
   * preceding <code>INPUT_METHOD_TEXT_CHANGED</code> event otherwise.
   * <p>Note that passing in an invalid <code>id</code> results in
   * unspecified behavior. This method throws an
   * <code>IllegalArgumentException</code> if <code>source</code>
   * is <code>null</code>.
   *
   * @param source the object where the event originated
   * @param id the event type
   * @param when a long integer that specifies the time the event occurred
   * @param text the combined committed and composed text, committed text first; must be
   * <code>null</code> when the event type is <code>CARET_POSITION_CHANGED</code>; may be
   * <code>null</code> for <code>INPUT_METHOD_TEXT_CHANGED</code> if there's no committed or
   * composed text
   * @param committedCharacterCount the number of committed characters in the text
   * @param caret the caret (a.k.a. insertion point); <code>null</code> if there's no caret within
   * current composed text
   * @param visiblePosition the position that's most important to be visible; <code>null</code> if
   * there's no recommendation for a visible position within current composed text
   * @throws IllegalArgumentException if <code>id</code> is not in the range
   * <code>INPUT_METHOD_FIRST</code>..<code>INPUT_METHOD_LAST</code>; or if id is
   * <code>CARET_POSITION_CHANGED</code> and <code>text</code> is not <code>null</code>; or if
   * <code>committedCharacterCount</code> is not in the range <code>0</code>..<code>(text.getEndIndex()
   * - text.getBeginIndex())</code>
   * @throws IllegalArgumentException if <code>source</code> is null
   * @since 1.4
   */
  public InputMethodEvent(Component source, int id, long when,
      AttributedCharacterIterator text, int committedCharacterCount,
      TextHitInfo caret, TextHitInfo visiblePosition) {
    super(source, id);
    if (id < INPUT_METHOD_FIRST || id > INPUT_METHOD_LAST) {
      throw new IllegalArgumentException("id outside of valid range");
    }

    if (id == CARET_POSITION_CHANGED && text != null) {
      throw new IllegalArgumentException("text must be null for CARET_POSITION_CHANGED");
    }

    this.when = when;
    this.text = text;
    int textLength = 0;
    if (text != null) {
      textLength = text.getEndIndex() - text.getBeginIndex();
    }

    if (committedCharacterCount < 0 || committedCharacterCount > textLength) {
      throw new IllegalArgumentException("committedCharacterCount outside of valid range");
    }
    this.committedCharacterCount = committedCharacterCount;

    this.caret = caret;
    this.visiblePosition = visiblePosition;
  }

  /**
   * Constructs an <code>InputMethodEvent</code> with the specified
   * source component, type, text, caret, and visiblePosition.
   * <p>
   * The offsets of caret and visiblePosition are relative to the current
   * composed text; that is, the composed text within <code>text</code>
   * if this is an <code>INPUT_METHOD_TEXT_CHANGED</code> event,
   * the composed text within the <code>text</code> of the
   * preceding <code>INPUT_METHOD_TEXT_CHANGED</code> event otherwise.
   * The time stamp for this event is initialized by invoking
   * {@link java.awt.EventQueue#getMostRecentEventTime()}.
   * <p>Note that passing in an invalid <code>id</code> results in
   * unspecified behavior. This method throws an
   * <code>IllegalArgumentException</code> if <code>source</code>
   * is <code>null</code>.
   *
   * @param source the object where the event originated
   * @param id the event type
   * @param text the combined committed and composed text, committed text first; must be
   * <code>null</code> when the event type is <code>CARET_POSITION_CHANGED</code>; may be
   * <code>null</code> for <code>INPUT_METHOD_TEXT_CHANGED</code> if there's no committed or
   * composed text
   * @param committedCharacterCount the number of committed characters in the text
   * @param caret the caret (a.k.a. insertion point); <code>null</code> if there's no caret within
   * current composed text
   * @param visiblePosition the position that's most important to be visible; <code>null</code> if
   * there's no recommendation for a visible position within current composed text
   * @throws IllegalArgumentException if <code>id</code> is not in the range
   * <code>INPUT_METHOD_FIRST</code>..<code>INPUT_METHOD_LAST</code>; or if id is
   * <code>CARET_POSITION_CHANGED</code> and <code>text</code> is not <code>null</code>; or if
   * <code>committedCharacterCount</code> is not in the range <code>0</code>..<code>(text.getEndIndex()
   * - text.getBeginIndex())</code>
   * @throws IllegalArgumentException if <code>source</code> is null
   */
  public InputMethodEvent(Component source, int id,
      AttributedCharacterIterator text, int committedCharacterCount,
      TextHitInfo caret, TextHitInfo visiblePosition) {
    this(source, id,
        getMostRecentEventTimeForSource(source),
        text, committedCharacterCount,
        caret, visiblePosition);
  }

  /**
   * Constructs an <code>InputMethodEvent</code> with the
   * specified source component, type, caret, and visiblePosition.
   * The text is set to <code>null</code>,
   * <code>committedCharacterCount</code> to 0.
   * <p>
   * The offsets of <code>caret</code> and <code>visiblePosition</code>
   * are relative to the current composed text; that is,
   * the composed text within the <code>text</code> of the
   * preceding <code>INPUT_METHOD_TEXT_CHANGED</code> event if the
   * event being constructed as a <code>CARET_POSITION_CHANGED</code> event.
   * For an <code>INPUT_METHOD_TEXT_CHANGED</code> event without text,
   * <code>caret</code> and <code>visiblePosition</code> must be
   * <code>null</code>.
   * The time stamp for this event is initialized by invoking
   * {@link java.awt.EventQueue#getMostRecentEventTime()}.
   * <p>Note that passing in an invalid <code>id</code> results in
   * unspecified behavior. This method throws an
   * <code>IllegalArgumentException</code> if <code>source</code>
   * is <code>null</code>.
   *
   * @param source the object where the event originated
   * @param id the event type
   * @param caret the caret (a.k.a. insertion point); <code>null</code> if there's no caret within
   * current composed text
   * @param visiblePosition the position that's most important to be visible; <code>null</code> if
   * there's no recommendation for a visible position within current composed text
   * @throws IllegalArgumentException if <code>id</code> is not in the range
   * <code>INPUT_METHOD_FIRST</code>..<code>INPUT_METHOD_LAST</code>
   * @throws IllegalArgumentException if <code>source</code> is null
   */
  public InputMethodEvent(Component source, int id, TextHitInfo caret,
      TextHitInfo visiblePosition) {
    this(source, id,
        getMostRecentEventTimeForSource(source),
        null, 0, caret, visiblePosition);
  }

  /**
   * Gets the combined committed and composed text.
   * Characters from index 0 to index <code>getCommittedCharacterCount() - 1</code> are committed
   * text, the remaining characters are composed text.
   *
   * @return the text. Always null for CARET_POSITION_CHANGED; may be null for
   * INPUT_METHOD_TEXT_CHANGED if there's no composed or committed text.
   */
  public AttributedCharacterIterator getText() {
    return text;
  }

  /**
   * Gets the number of committed characters in the text.
   */
  public int getCommittedCharacterCount() {
    return committedCharacterCount;
  }

  /**
   * Gets the caret.
   * <p>
   * The offset of the caret is relative to the current
   * composed text; that is, the composed text within getText()
   * if this is an <code>INPUT_METHOD_TEXT_CHANGED</code> event,
   * the composed text within getText() of the
   * preceding <code>INPUT_METHOD_TEXT_CHANGED</code> event otherwise.
   *
   * @return the caret (a.k.a. insertion point). Null if there's no caret within current composed
   * text.
   */
  public TextHitInfo getCaret() {
    return caret;
  }

  /**
   * Gets the position that's most important to be visible.
   * <p>
   * The offset of the visible position is relative to the current
   * composed text; that is, the composed text within getText()
   * if this is an <code>INPUT_METHOD_TEXT_CHANGED</code> event,
   * the composed text within getText() of the
   * preceding <code>INPUT_METHOD_TEXT_CHANGED</code> event otherwise.
   *
   * @return the position that's most important to be visible. Null if there's no recommendation for
   * a visible position within current composed text.
   */
  public TextHitInfo getVisiblePosition() {
    return visiblePosition;
  }

  /**
   * Consumes this event so that it will not be processed
   * in the default manner by the source which originated it.
   */
  public void consume() {
    consumed = true;
  }

  /**
   * Returns whether or not this event has been consumed.
   *
   * @see #consume
   */
  public boolean isConsumed() {
    return consumed;
  }

  /**
   * Returns the time stamp of when this event occurred.
   *
   * @return this event's timestamp
   * @since 1.4
   */
  public long getWhen() {
    return when;
  }

  /**
   * Returns a parameter string identifying this event.
   * This method is useful for event-logging and for debugging.
   * It contains the event ID in text form, the characters of the
   * committed and composed text
   * separated by "+", the number of committed characters,
   * the caret, and the visible position.
   *
   * @return a string identifying the event and its attributes
   */
  public String paramString() {
    String typeStr;
    switch (id) {
      case INPUT_METHOD_TEXT_CHANGED:
        typeStr = "INPUT_METHOD_TEXT_CHANGED";
        break;
      case CARET_POSITION_CHANGED:
        typeStr = "CARET_POSITION_CHANGED";
        break;
      default:
        typeStr = "unknown type";
    }

    String textString;
    if (text == null) {
      textString = "no text";
    } else {
      StringBuilder textBuffer = new StringBuilder("\"");
      int committedCharacterCount = this.committedCharacterCount;
      char c = text.first();
      while (committedCharacterCount-- > 0) {
        textBuffer.append(c);
        c = text.next();
      }
      textBuffer.append("\" + \"");
      while (c != CharacterIterator.DONE) {
        textBuffer.append(c);
        c = text.next();
      }
      textBuffer.append("\"");
      textString = textBuffer.toString();
    }

    String countString = committedCharacterCount + " characters committed";

    String caretString;
    if (caret == null) {
      caretString = "no caret";
    } else {
      caretString = "caret: " + caret.toString();
    }

    String visiblePositionString;
    if (visiblePosition == null) {
      visiblePositionString = "no visible position";
    } else {
      visiblePositionString = "visible position: " + visiblePosition.toString();
    }

    return typeStr + ", " + textString + ", " + countString + ", " + caretString + ", "
        + visiblePositionString;
  }

  /**
   * Initializes the <code>when</code> field if it is not present in the
   * object input stream. In that case, the field will be initialized by
   * invoking {@link java.awt.EventQueue#getMostRecentEventTime()}.
   */
  private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
    s.defaultReadObject();
    if (when == 0) {
      // Can't use getMostRecentEventTimeForSource because source is always null during deserialization
      when = EventQueue.getMostRecentEventTime();
    }
  }

  /**
   * Get the most recent event time in the {@code EventQueue} which the {@code source}
   * belongs to.
   *
   * @param source the source of the event
   * @return most recent event time in the {@code EventQueue}
   * @throws IllegalArgumentException if source is null.
   */
  private static long getMostRecentEventTimeForSource(Object source) {
    if (source == null) {
      // throw the IllegalArgumentException to conform to EventObject spec
      throw new IllegalArgumentException("null source");
    }
    AppContext appContext = SunToolkit.targetToAppContext(source);
    EventQueue eventQueue = SunToolkit.getSystemEventQueueImplPP(appContext);
    return AWTAccessor.getEventQueueAccessor().getMostRecentEventTime(eventQueue);
  }
}
